home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 March / PCWorld_2008-03_cd.bin / v cisle / mobiDVD / MobiDVD-1.0.0.6.exe / xulrunner / components / nsLoginManager.js < prev    next >
Text File  |  2007-07-20  |  46KB  |  1,308 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is mozilla.org code.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla Corporation.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Justin Dolske <dolske@mozilla.com> (original author)
  22.  *
  23.  * Alternatively, the contents of this file may be used under the terms of
  24.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  25.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26.  * in which case the provisions of the GPL or the LGPL are applicable instead
  27.  * of those above. If you wish to allow use of your version of this file only
  28.  * under the terms of either the GPL or the LGPL, and not to allow others to
  29.  * use your version of this file under the terms of the MPL, indicate your
  30.  * decision by deleting the provisions above and replace them with the notice
  31.  * and other provisions required by the GPL or the LGPL. If you do not delete
  32.  * the provisions above, a recipient may use your version of this file under
  33.  * the terms of any one of the MPL, the GPL or the LGPL.
  34.  *
  35.  * ***** END LICENSE BLOCK ***** */
  36.  
  37.  
  38. const Cc = Components.classes;
  39. const Ci = Components.interfaces;
  40.  
  41. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  42.  
  43. function LoginManager() {
  44.     this.init();
  45. }
  46.  
  47. LoginManager.prototype = {
  48.  
  49.     classDescription: "LoginManager",
  50.     contractID: "@mozilla.org/login-manager;1",
  51.     classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
  52.     QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
  53.                                             Ci.nsISupportsWeakReference]),
  54.  
  55.  
  56.     /* ---------- private memebers ---------- */
  57.  
  58.  
  59.     __logService : null, // Console logging service, used for debugging.
  60.     get _logService() {
  61.         if (!this.__logService)
  62.             this.__logService = Cc["@mozilla.org/consoleservice;1"]
  63.                                     .getService(Ci.nsIConsoleService);
  64.         return this.__logService;
  65.     },
  66.  
  67.  
  68.     __promptService : null, // Prompt service for user interaction
  69.     get _promptService() {
  70.         if (!this.__promptService)
  71.             this.__promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]
  72.                                         .getService(Ci.nsIPromptService2);
  73.         return this.__promptService;
  74.     },
  75.  
  76.  
  77.     __ioService: null, // IO service for string -> nsIURI conversion
  78.     get _ioService() {
  79.         if (!this.__ioService)
  80.             this.__ioService = Cc["@mozilla.org/network/io-service;1"]
  81.                                     .getService(Ci.nsIIOService);
  82.         return this.__ioService;
  83.     },
  84.  
  85.  
  86.     __formFillService : null, // FormFillController, for username autocompleting
  87.     get _formFillService() {
  88.         if (!this.__formFillService)
  89.             this.__formFillService = Cc[
  90.                                 "@mozilla.org/satchel/form-fill-controller;1"]
  91.                                     .getService(Ci.nsIFormFillController);
  92.         return this.__formFillService;
  93.     },
  94.  
  95.  
  96.     __strBundle : null, // String bundle for L10N
  97.     get _strBundle() {
  98.         if (!this.__strBundle) {
  99.             var bunService = Cc["@mozilla.org/intl/stringbundle;1"]
  100.                                     .getService(Ci.nsIStringBundleService);
  101.             this.__strBundle = bunService.createBundle(
  102.                         "chrome://passwordmgr/locale/passwordmgr.properties");
  103.             if (!this.__strBundle)
  104.                 throw "String bundle for Login Manager not present!";
  105.         }
  106.  
  107.         return this.__strBundle;
  108.     },
  109.  
  110.  
  111.     __brandBundle : null, // String bundle for L10N
  112.     get _brandBundle() {
  113.         if (!this.__brandBundle) {
  114.             var bunService = Cc["@mozilla.org/intl/stringbundle;1"]
  115.                                     .getService(Ci.nsIStringBundleService);
  116.             this.__brandBundle = bunService.createBundle(
  117.                         "chrome://branding/locale/brand.properties");
  118.             if (!this.__brandBundle)
  119.                 throw "Branding string bundle not present!";
  120.         }
  121.  
  122.         return this.__brandBundle;
  123.     },
  124.  
  125.  
  126.     __storage : null, // Storage component which contains the saved logins
  127.     get _storage() {
  128.         if (!this.__storage) {
  129.             this.__storage = Cc["@mozilla.org/login-manager/storage/legacy;1"]
  130.                                 .createInstance(Ci.nsILoginManagerStorage);
  131.             try {
  132.                 this.__storage.init();
  133.             } catch (e) {
  134.                 this.log("Initialization of storage component failed: " + e);
  135.                 this.__storage = null;
  136.             }
  137.         }
  138.  
  139.         return this.__storage;
  140.     },
  141.  
  142.     _prefBranch : null, // Preferences service
  143.     _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
  144.  
  145.     _remember : true,  // mirrors signon.rememberSignons preference
  146.     _debug    : false, // mirrors signon.debug
  147.  
  148.  
  149.     /*
  150.      * init
  151.      *
  152.      * Initialize the Login Manager. Automatically called when service
  153.      * is created.
  154.      *
  155.      * Note: Service created in /browser/base/content/browser.js,
  156.      *       delayedStartup()
  157.      */
  158.     init : function () {
  159.  
  160.         // Cache references to current |this| in utility objects
  161.         this._webProgressListener._domEventListener = this._domEventListener;
  162.         this._webProgressListener._pwmgr = this;
  163.         this._domEventListener._pwmgr    = this;
  164.         this._observer._pwmgr            = this;
  165.  
  166.         // Preferences. Add observer so we get notified of changes.
  167.         this._prefBranch = Cc["@mozilla.org/preferences-service;1"]
  168.             .getService(Ci.nsIPrefService).getBranch("signon.");
  169.         this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
  170.         this._prefBranch.addObserver("", this._observer, false);
  171.  
  172.         // Get current preference values.
  173.         this._debug = this._prefBranch.getBoolPref("debug");
  174.  
  175.         this._remember = this._prefBranch.getBoolPref("rememberSignons");
  176.  
  177.  
  178.         // Get constructor for nsILoginInfo
  179.         this._nsLoginInfo = new Components.Constructor(
  180.             "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
  181.  
  182.  
  183.         // Form submit observer checks forms for new logins and pw changes.
  184.         var observerService = Cc["@mozilla.org/observer-service;1"]
  185.                                 .getService(Ci.nsIObserverService);
  186.         observerService.addObserver(this._observer, "earlyformsubmit", false);
  187.         observerService.addObserver(this._observer, "xpcom-shutdown", false);
  188.  
  189.         // WebProgressListener for getting notification of new doc loads.
  190.         var progress = Cc["@mozilla.org/docloaderservice;1"]
  191.                         .getService(Ci.nsIWebProgress);
  192.         progress.addProgressListener(this._webProgressListener,
  193.                                      Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
  194.  
  195.  
  196.     },
  197.  
  198.  
  199.     /*
  200.      * log
  201.      *
  202.      * Internal function for logging debug messages to the Error Console window
  203.      */
  204.     log : function (message) {
  205.         if (!this._debug)
  206.             return;
  207.         dump("Login Manager: " + message + "\n");
  208.         this._logService.logStringMessage("Login Manager: " + message);
  209.     },
  210.  
  211.  
  212.     /* ---------- Utility objects ---------- */
  213.  
  214.  
  215.     /*
  216.      * _observer object
  217.      *
  218.      * Internal utility object, implements the nsIObserver interface.
  219.      * Used to receive notification for: form submission, preference changes.
  220.      */
  221.     _observer : {
  222.         _pwmgr : null,
  223.  
  224.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 
  225.                                                 Ci.nsIFormSubmitObserver,
  226.                                                 Ci.nsISupportsWeakReference]),
  227.  
  228.  
  229.         // nsFormSubmitObserver
  230.         notify : function (formElement, aWindow, actionURI) {
  231.             this._pwmgr.log("observer notified for form submission.");
  232.  
  233.             // We're invoked before the content's |onsubmit| handlers, so we
  234.             // can grab form data before it might be modified (see bug 257781).
  235.  
  236.             try {
  237.                 this._pwmgr._onFormSubmit(formElement);
  238.             } catch (e) {
  239.                 this._pwmgr.log("Caught error in onFormSubmit: " + e);
  240.             }
  241.  
  242.             return true; // Always return true, or form submt will be canceled.
  243.         },
  244.  
  245.         // nsObserver
  246.         observe : function (subject, topic, data) {
  247.  
  248.             if (topic == "nsPref:changed") {
  249.                 var prefName = data;
  250.                 this._pwmgr.log("got change to " + prefName + " preference");
  251.  
  252.                 if (prefName == "debug") {
  253.                     this._pwmgr._debug = 
  254.                         this._pwmgr._prefBranch.getBoolPref("debug");
  255.                 } else if (prefName == "rememberSignons") {
  256.                     this._pwmgr._remember =
  257.                         this._pwmgr._prefBranch.getBoolPref("rememberSignons");
  258.                 } else {
  259.                     this._pwmgr.log("Oops! Pref not handled, change ignored.");
  260.                 }
  261.             } else if (topic == "xpcom-shutdown") {
  262.                 for (let i in this._pwmgr) {
  263.                   try {
  264.                     this._pwmgr[i] = null;
  265.                   } catch(ex) {}
  266.                 }
  267.                 this._pwmgr = null;
  268.             } else {
  269.                 this._pwmgr.log("Oops! Unexpected notification: " + topic);
  270.             }
  271.         }
  272.     },
  273.  
  274.  
  275.     /*
  276.      * _webProgressListener object
  277.      *
  278.      * Internal utility object, implements nsIWebProgressListener interface.
  279.      * This is attached to the document loader service, so we get
  280.      * notifications about all page loads.
  281.      */
  282.     _webProgressListener : {
  283.         _pwmgr : null,
  284.         _domEventListener : null,
  285.  
  286.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
  287.                                                 Ci.nsISupportsWeakReference]),
  288.  
  289.  
  290.         onStateChange : function (aWebProgress, aRequest,
  291.                                   aStateFlags,  aStatus) {
  292.  
  293.             // STATE_START is too early, doc is still the old page.
  294.             if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
  295.                 return 0;
  296.  
  297.             if (!this._pwmgr._remember)
  298.                 return 0;
  299.  
  300.             var domWin = aWebProgress.DOMWindow;
  301.             var domDoc = domWin.document;
  302.  
  303.             // Only process things which might have HTML forms.
  304.             if (! domDoc instanceof Ci.nsIDOMHTMLDocument)
  305.                 return 0;
  306.  
  307.             this._pwmgr.log("onStateChange accepted: req = " + (aRequest ?
  308.                         aRequest.name : "(null)") + ", flags = " + aStateFlags);
  309.  
  310.             // fastback navigation... We won't get a DOMContentLoaded
  311.             // event again, so process any forms now.
  312.             if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
  313.                 this._pwmgr.log("onStateChange: restoring document");
  314.                 return this._pwmgr._fillDocument(domDoc);
  315.             }
  316.  
  317.             // Add event listener to process page when DOM is complete.
  318.             this._pwmgr.log("onStateChange: adding dom listeners");
  319.             domDoc.addEventListener("DOMContentLoaded",
  320.                                     this._domEventListener, false);
  321.             return 0;
  322.         },
  323.  
  324.         // stubs for the nsIWebProgressListener interfaces which we don't use.
  325.         onProgressChange : function() { throw "Unexpected onProgressChange"; },
  326.         onLocationChange : function() { throw "Unexpected onLocationChange"; },
  327.         onStatusChange   : function() { throw "Unexpected onStatusChange";   },
  328.         onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
  329.     },
  330.  
  331.  
  332.     /*
  333.      * _domEventListener object
  334.      *
  335.      * Internal utility object, implements nsIDOMEventListener
  336.      * Used to catch certain DOM events needed to properly implement form fill.
  337.      */
  338.     _domEventListener : {
  339.         _pwmgr : null,
  340.  
  341.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
  342.                                                 Ci.nsISupportsWeakReference]),
  343.  
  344.  
  345.         handleEvent : function (event) {
  346.             this._pwmgr.log("domEventListener: got event " + event.type);
  347.  
  348.             var doc, inputElement;
  349.             switch (event.type) {
  350.                 case "DOMContentLoaded":
  351.                     doc = event.target;
  352.                     return this._pwmgr._fillDocument(doc);
  353.  
  354.                 case "DOMAutoComplete":
  355.                 case "blur":
  356.                     inputElement = event.target;
  357.                     return this._pwmgr._fillPassword(inputElement);
  358.  
  359.                 default:
  360.                     this._pwmgr.log("Oops! This event unexpected.");
  361.                     return 0;
  362.             }
  363.         }
  364.     },
  365.  
  366.  
  367.  
  368.  
  369.     /* ---------- Primary Public interfaces ---------- */
  370.  
  371.  
  372.  
  373.  
  374.     /*
  375.      * addLogin
  376.      *
  377.      * Add a new login to login storage.
  378.      */
  379.     addLogin : function (login) {
  380.         // Sanity check the login
  381.         if (login.hostname == null || login.hostname.length == 0)
  382.             throw "Can't add a login with a null or empty hostname.";
  383.  
  384.         if (login.username == null || login.username.length == 0)
  385.             throw "Can't add a login with a null or empty username.";
  386.  
  387.         if (login.password == null || login.password.length == 0)
  388.             throw "Can't add a login with a null or empty password.";
  389.  
  390.         if (!login.httpRealm && !login.formSubmitURL)
  391.             throw "Can't add a login without a httpRealm or formSubmitURL.";
  392.  
  393.         // Look for an existing entry.
  394.         var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
  395.                                      login.httpRealm);
  396.  
  397.         if (logins.some(function(l) { return login.username == l.username }))
  398.             throw "This login already exists.";
  399.  
  400.         this.log("Adding login: " + login);
  401.         return this._storage.addLogin(login);
  402.     },
  403.  
  404.  
  405.     /*
  406.      * removeLogin
  407.      *
  408.      * Remove the specified login from the stored logins.
  409.      */
  410.     removeLogin : function (login) {
  411.         this.log("Removing login: " + login);
  412.         return this._storage.removeLogin(login);
  413.     },
  414.  
  415.  
  416.     /*
  417.      * modifyLogin
  418.      *
  419.      * Change the specified login to match the new login.
  420.      */
  421.     modifyLogin : function (oldLogin, newLogin) {
  422.         this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
  423.         return this._storage.modifyLogin(oldLogin, newLogin);
  424.     },
  425.  
  426.  
  427.     /*
  428.      * getAllLogins
  429.      *
  430.      * Get a dump of all stored logins. Used by the login manager UI.
  431.      *
  432.      * |count| is only needed for XPCOM.
  433.      *
  434.      * Returns an array of logins. If there are no logins, the array is empty.
  435.      */
  436.     getAllLogins : function (count) {
  437.         this.log("Getting a list of all logins");
  438.         var logins = this._storage.getAllLogins({});
  439.         count.value = logins.length;
  440.         return logins;
  441.     },
  442.  
  443.  
  444.     /*
  445.      * removeAllLogins
  446.      *
  447.      * Remove all stored logins.
  448.      */
  449.     removeAllLogins : function () {
  450.         this.log("Removing all logins");
  451.         this._storage.removeAllLogins();
  452.     },
  453.  
  454.     /*
  455.      * getAllDisabledHosts
  456.      *
  457.      * Get a list of all hosts for which logins are disabled.
  458.      *
  459.      * |count| is only needed for XPCOM.
  460.      *
  461.      * Returns an array of disabled logins. If there are no disabled logins,
  462.      * the array is empty.
  463.      */
  464.     getAllDisabledHosts : function (count) {
  465.         this.log("Getting a list of all disabled hosts");
  466.         var hosts = this._storage.getAllDisabledHosts({});
  467.         count.value = hosts.length;
  468.         return hosts;
  469.     },
  470.  
  471.  
  472.     /*
  473.      * findLogins
  474.      *
  475.      * Search the known logins for entries matching the specified criteria
  476.      * for a protocol login (eg HTTP Auth).
  477.      */
  478.     findLogins : function (count, hostname, formSubmitURL, httpRealm) {
  479.         this.log("Searching for logins matching host: " + hostname +
  480.             ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
  481.  
  482.         var logins = this._storage.findLogins({}, hostname, formSubmitURL,
  483.                                               httpRealm);
  484.         count.value = logins.length;
  485.         return logins;
  486.     },
  487.  
  488.  
  489.     /*
  490.      * getLoginSavingEnabled
  491.      *
  492.      * Check to see if user has disabled saving logins for the host.
  493.      */
  494.     getLoginSavingEnabled : function (host) {
  495.         this.log("Checking if logins to " + host + " can be saved.");
  496.         if (!this._remember)
  497.             return false;
  498.  
  499.         return this._storage.getLoginSavingEnabled(host);
  500.     },
  501.  
  502.  
  503.     /*
  504.      * setLoginSavingEnabled
  505.      *
  506.      * Enable or disable storing logins for the specified host.
  507.      */
  508.     setLoginSavingEnabled : function (hostname, enabled) {
  509.         this.log("Saving logins for " + hostname + " enabled? " + enabled);
  510.         return this._storage.setLoginSavingEnabled(hostname, enabled);
  511.     },
  512.  
  513.  
  514.     /*
  515.      * autoCompleteSearch
  516.      *
  517.      * Yuck. This is called directly by satchel:
  518.      * nsFormFillController::StartSearch()
  519.      * [toolkit/components/satchel/src/nsFormFillController.cpp]
  520.      *
  521.      * We really ought to have a simple way for code to register an
  522.      * auto-complete provider, and not have satchel calling pwmgr directly.
  523.      */
  524.     autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
  525.         // aPreviousResult & aResult are nsIAutoCompleteResult,
  526.         // aElement is nsIDOMHTMLInputElement
  527.  
  528.         if (!this._remember)
  529.             return false;
  530.  
  531.         this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
  532.  
  533.         var result = null;
  534.  
  535.         if (aPreviousResult) {
  536.             this.log("Using previous autocomplete result");
  537.             result = aPreviousResult;
  538.  
  539.             // We have a list of results for a shorter search string, so just
  540.             // filter them further based on the new search string.
  541.             // Count backwards, because result.matchCount is decremented
  542.             // when we remove an entry.
  543.             for (var i = result.matchCount - 1; i >= 0; i--) {
  544.                 var match = result.getValueAt(i);
  545.  
  546.                 // Remove results that are too short, or have different prefix.
  547.                 if (aSearchString.length > match.length ||
  548.                     aSearchString.toLowerCase() !=
  549.                         match.substr(0, aSearchString.length).toLowerCase())
  550.                 {
  551.                     this.log("Removing autocomplete entry '" + match + "'");
  552.                     result.removeValueAt(i, false);
  553.                 }
  554.             }
  555.         } else {
  556.             // XXX The C++ code took care to avoid reentrancy if a
  557.             // master-password dialog was triggered here, but since
  558.             // we're decrypting at load time that can't happen right now.
  559.             this.log("Creating new autocomplete search result.");
  560.  
  561.             var doc = aElement.ownerDocument;
  562.             var origin = this._getPasswordOrigin(doc.documentURI);
  563.             var actionOrigin = this._getActionOrigin(aElement.form);
  564.  
  565.             var logins = this.findLogins({}, origin, actionOrigin, null);
  566.             var matchingLogins = [];
  567.  
  568.             for (i = 0; i < logins.length; i++) {
  569.                 var username = logins[i].username.toLowerCase();
  570.                 if (aSearchString.length <= username.length &&
  571.                     aSearchString.toLowerCase() ==
  572.                         username.substr(0, aSearchString.length))
  573.                 {
  574.                     matchingLogins.push(logins[i]);
  575.                 }
  576.             }
  577.             this.log(matchingLogins.length + " autocomplete logins avail.");
  578.             result = new UserAutoCompleteResult(aSearchString, matchingLogins);
  579.         }
  580.  
  581.         return result;
  582.     },
  583.  
  584.  
  585.  
  586.  
  587.     /* ------- Internal methods / callbacks for document integration ------- */
  588.  
  589.  
  590.  
  591.  
  592.     /*
  593.      * _getPasswordFields
  594.      *
  595.      * Returns an array of password field elements for the specified form.
  596.      * If no pw fields are found, or if more than 3 are found, then null
  597.      * is returned.
  598.      *
  599.      * skipEmptyFields can be set to ignore password fields with no value.
  600.      */
  601.     _getPasswordFields : function (form, skipEmptyFields) {
  602.         // Locate the password fields in the form.
  603.         var pwFields = [];
  604.         for (var i = 0; i < form.elements.length; i++) {
  605.             if (form.elements[i].type != "password")
  606.                 continue;
  607.  
  608.             if (skipEmptyFields && !form.elements[i].value)
  609.                 continue;
  610.  
  611.             pwFields[pwFields.length] = {
  612.                                             index   : i,
  613.                                             element : form.elements[i]
  614.                                         };
  615.         }
  616.  
  617.         // If too few or too many fields, bail out.
  618.         if (pwFields.length == 0 || pwFields.length > 3)
  619.             return null;
  620.  
  621.         return pwFields;
  622.     },
  623.  
  624.  
  625.     /*
  626.      * _getFormFields
  627.      *
  628.      * Returns the username and password fields found in the form.
  629.      * Can handle complex forms by trying to figure out what the
  630.      * relevant fields are.
  631.      *
  632.      * Returns: [usernameField, newPasswordField, oldPasswordField]
  633.      *
  634.      * usernameField may be null.
  635.      * newPasswordField will always be non-null.
  636.      * oldPasswordField may be null. If null, newPasswordField is just
  637.      * "theLoginField". If not null, the form is apparently a
  638.      * change-password field, with oldPasswordField containing the password
  639.      * that is being changed.
  640.      */
  641.     _getFormFields : function (form, isSubmission) {
  642.         // Locate the password field(s) in the form. Up to 3 supported.
  643.         // If there's no password field, there's nothing for us to do.
  644.         var pwFields = this._getPasswordFields(form, isSubmission);
  645.         if (!pwFields) {
  646.             this.log("(form ignored -- either 0 or >3 pw fields.)");
  647.             return [null, null, null];
  648.         }
  649.  
  650.  
  651.         // Locate the username field in the form by serarching backwards
  652.         // from the first passwordfield, assume the first text field is the
  653.         // username. We might not find a username field if the user is
  654.         // already logged in to the site. 
  655.         for (var i = pwFields[0].index - 1; i >= 0; i--) {
  656.             if (form.elements[i].type == "text") {
  657.                 var usernameField = form.elements[i];
  658.                 break;
  659.             }
  660.         }
  661.  
  662.         if (!usernameField)
  663.             this.log("(form -- no username field found)");
  664.  
  665.  
  666.         // If we're not submitting a form (it's a page load), there are no
  667.         // password field values for us to use for identifying fields. So,
  668.         // just assume the first password field is the one to be filled in.
  669.         if (!isSubmission || pwFields.length == 1)
  670.             return [usernameField, pwFields[0].element, null];
  671.  
  672.  
  673.         // Try to figure out WTF is in the form based on the password values.
  674.         var oldPasswordField, newPasswordField;
  675.         var pw1 = pwFields[0].element.value;
  676.         var pw2 = pwFields[1].element.value;
  677.         var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
  678.  
  679.         if (pwFields.length == 3) {
  680.             // Look for two identical passwords, that's the new password
  681.  
  682.             if (pw1 == pw2 && pw2 == pw3) {
  683.                 // All 3 passwords the same? Weird! Treat as if 1 pw field.
  684.                 newPasswordField = pwFields[0].element;
  685.                 oldPasswordField = null;
  686.             } else if (pw1 == pw2) {
  687.                 newPasswordField = pwFields[0].element;
  688.                 oldPasswordField = pwFields[2].element;
  689.             } else if (pw2 == pw3) {
  690.                 oldPasswordField = pwFields[0].element;
  691.                 newPasswordField = pwFields[2].element;
  692.             } else  if (pw1 == pw3) {
  693.                 // A bit odd, but could make sense with the right page layout.
  694.                 newPasswordField = pwFields[0].element;
  695.                 oldPasswordField = pwFields[1].element;
  696.             } else {
  697.                 // We can't tell which of the 3 passwords should be saved.
  698.                 this.log("(form ignored -- all 3 pw fields differ)");
  699.                 return [null, null, null];
  700.             }
  701.         } else { // pwFields.length == 2
  702.             if (pw1 == pw2) {
  703.                 // Treat as if 1 pw field
  704.                 newPasswordField = pwFields[0].element;
  705.                 oldPasswordField = null;
  706.             } else {
  707.                 // Just assume that the 2nd password is the new password
  708.                 oldPasswordField = pwFields[0].element;
  709.                 newPasswordField = pwFields[1].element;
  710.             }
  711.         }
  712.  
  713.         return [usernameField, newPasswordField, oldPasswordField];
  714.     },
  715.  
  716.  
  717.     /*
  718.      * _onFormSubmit
  719.      *
  720.      * Called by the our observer when notified of a form submission.
  721.      * [Note that this happens before any DOM onsubmit handlers are invoked.]
  722.      * Looks for a password change in the submitted form, so we can update
  723.      * our stored password.
  724.      *
  725.      * XXX update actionURL of existing login, even if pw not being changed?
  726.      */
  727.     _onFormSubmit : function (form) {
  728.  
  729.         // local helper function
  730.         function autocompleteDisabled(element) {
  731.             if (element && element.hasAttribute("autocomplete") &&
  732.                 element.getAttribute("autocomplete").toLowerCase() == "off")
  733.                 return true;
  734.  
  735.            return false;
  736.         };
  737.  
  738.         // local helper function
  739.         function findExistingLogin(pwmgr, hostname,
  740.                                    formSubmitURL, usernameField) {
  741.  
  742.             var searchLogin = new pwmgr._nsLoginInfo();
  743.             searchLogin.init(hostname, formSubmitURL, null,
  744.                              usernameField.value, "",
  745.                              usernameField.name,  "");
  746.  
  747.             var logins = pwmgr.findLogins({}, hostname, formSubmitURL, null);
  748.             var existingLogin;
  749.             var found = logins.some(function(l) {
  750.                                     existingLogin = l;
  751.                                     return searchLogin.equalsIgnorePassword(l);
  752.                                 });
  753.  
  754.             return (found ? existingLogin : null);
  755.         }
  756.  
  757.  
  758.  
  759.  
  760.         var doc = form.ownerDocument;
  761.         var win = doc.window;
  762.  
  763.         // If password saving is disabled (globally or for host), bail out now.
  764.         if (!this._remember)
  765.             return;
  766.  
  767.         var hostname      = this._getPasswordOrigin(doc.documentURI);
  768.         var formSubmitURL = this._getActionOrigin(form)
  769.         if (!this.getLoginSavingEnabled(hostname)) {
  770.             this.log("(form submission ignored -- saving is " +
  771.                      "disabled for: " + hostname + ")");
  772.             return;
  773.         }
  774.  
  775.  
  776.         // Get the appropriate fields from the form.
  777.         var [usernameField, newPasswordField, oldPasswordField] =
  778.             this._getFormFields(form, true);
  779.  
  780.         // Need at least 1 valid password field to do anything.
  781.         if (newPasswordField == null)
  782.                 return;
  783.  
  784.         // Check for autocomplete=off attribute. We don't use it to prevent
  785.         // autofilling (for existing logins), but won't save logins when it's
  786.         // present.
  787.         if (autocompleteDisabled(form) ||
  788.             autocompleteDisabled(usernameField) ||
  789.             autocompleteDisabled(newPasswordField) ||
  790.             autocompleteDisabled(oldPasswordField)) {
  791.                 this.log("(form submission ignored -- autocomplete=off found)");
  792.                 return;
  793.         }
  794.  
  795.  
  796.         var formLogin = new this._nsLoginInfo();
  797.         formLogin.init(hostname, formSubmitURL, null,
  798.                     (usernameField ? usernameField.value : null),
  799.                     newPasswordField.value,
  800.                     (usernameField ? usernameField.name  : null),
  801.                     newPasswordField.name);
  802.  
  803.         // If we didn't find a username field, but seem to be changing a
  804.         // password, allow the user to select from a list of applicable
  805.         // logins to update the password for.
  806.         if (!usernameField && oldPasswordField) {
  807.  
  808.             var ok, username;
  809.             var logins = this.findLogins({}, hostname, formSubmitURL, null);
  810.  
  811.             // XXX we could be smarter here: look for a login matching the
  812.             // old password value. If there's only one, update it. If there's
  813.             // more than one we could filter the list (but, edge case: the
  814.             // login for the pwchange is in pwmgr, but with an outdated
  815.             // password. and the user has another login, with the same
  816.             // password as the form login's old password.) ugh.
  817.             // XXX if you're changing a password, and there's no username
  818.             // in the form, then you can't add the login. Will need to change
  819.             // prompting to allow this.
  820.  
  821.             if (logins.length == 0) {
  822.                 this.log("(no logins for this host -- pwchange ignored)");
  823.                 return;
  824.             } else if (logins.length == 1) {
  825.                 username = logins[0].username;
  826.                 ok = this._promptToChangePassword(win, username)
  827.             } else {
  828.                 var usernames = [];
  829.                 logins.forEach(function(l) { usernames.push(l.username); });
  830.  
  831.                 [ok, username] = this._promptToChangePasswordWithUsernames(
  832.                                                                 win, usernames);
  833.             }
  834.  
  835.             if (!ok)
  836.                 return;
  837.  
  838.             // Now that we know the desired username, find that login and
  839.             // update the info in our formLogin representation.
  840.             this.log("Updating password for username " + username);
  841.  
  842.             var existingLogin;
  843.             logins.some(function(l) {
  844.                                     existingLogin = l;
  845.                                     return (l.username == username);
  846.                                 });
  847.  
  848.             formLogin.username      = username;
  849.             formLogin.usernameField = existingLogin.usernameField;
  850.  
  851.             this.modifyLogin(existingLogin, formLogin);
  852.             
  853.             return;
  854.         }
  855.  
  856.         if (!usernameField && !oldPasswordField) {
  857.             this.log("XXX not handled yet");
  858.             return;
  859.         }
  860.  
  861.         // We have a username. Look for an existing login that matches the
  862.         // form data (other than the password, which might be different)
  863.         existingLogin = findExistingLogin(this, hostname, formSubmitURL,
  864.                                           usernameField);
  865.         if (existingLogin) {
  866.             this.log("Found an existing login matching this form submission");
  867.  
  868.             // Change password if needed
  869.             if (existingLogin.password != formLogin.password) {
  870.                 this.log("...Updating password for existing login.");
  871.                 this.modifyLogin(existingLogin, formLogin);
  872.             }
  873.  
  874.             return;
  875.         }
  876.  
  877.  
  878.         // Prompt user to save a new login.
  879.         var userChoice = this._promptToSaveLogin(win);
  880.  
  881.         if (userChoice == 2) {
  882.             this.log("Disabling " + hostname + " logins by user request.");
  883.             this.setLoginSavingEnabled(hostname, false);
  884.         } else if (userChoice == 0) {
  885.             this.log("Saving login for " + hostname);
  886.             this.addLogin(formLogin);
  887.         } else {
  888.             // userChoice == 1 --> just ignore the login.
  889.             this.log("Ignoring login.");
  890.         }
  891.     },
  892.  
  893.  
  894.     /*
  895.      * _getPasswordOrigin
  896.      *
  897.      * Get the parts of the URL we want for identification.
  898.      */
  899.     _getPasswordOrigin : function (uriString) {
  900.         var realm = "";
  901.         try {
  902.             var uri = this._ioService.newURI(uriString, null, null);
  903.  
  904.             realm += uri.scheme;
  905.             realm += "://";
  906.             realm += uri.hostPort;
  907.         } catch (e) {
  908.             // bug 159484 - disallow url types that don't support a hostPort.
  909.             // (set null to cause throw in the JS above)
  910.             realm = null;
  911.         }
  912.  
  913.         return realm;
  914.     },
  915.  
  916.     _getActionOrigin : function (form) {
  917.         var uriString = form.action;
  918.  
  919.         // A blank or mission action submits to where it came from.
  920.         if (uriString == "")
  921.             uriString = form.baseURI; // ala bug 297761
  922.  
  923.         return this._getPasswordOrigin(uriString);
  924.     },
  925.  
  926.  
  927.     /*
  928.      * _fillDocument
  929.      *
  930.      * Called when a page has loaded. For each form in the document,
  931.      * we check to see if it can be filled with a stored login.
  932.      */
  933.     _fillDocument : function (doc) {
  934.         var forms = doc.forms;
  935.         if (!forms || forms.length == 0)
  936.             return; 
  937.  
  938.         var formOrigin = this._getPasswordOrigin(doc.documentURI);
  939.         var autofillForm = this._prefBranch.getBoolPref("autofillForms");
  940.  
  941.         this.log("fillDocument found " + forms.length +
  942.                  " forms on " + doc.documentURI);
  943.  
  944.         var previousActionOrigin = null;
  945.  
  946.         for (var i = 0; i < forms.length; i++) {
  947.             var form = forms[i];
  948.             var actionOrigin = this._getActionOrigin(form);
  949.  
  950.             // Heuristically determine what the user/pass fields are
  951.             // We do this before checking to see if logins are stored,
  952.             // so that the user isn't prompted for a master password
  953.             // without need.
  954.             var [usernameField, passwordField, ignored] =
  955.                 this._getFormFields(form, false);
  956.  
  957.             // Need a valid password field to do anything.
  958.             if (passwordField == null)
  959.                 continue;
  960.  
  961.  
  962.             // Only the actionOrigin might be changing, so if it's the same
  963.             // as the last form on the page we can reuse the same logins.
  964.             if (actionOrigin != previousActionOrigin) {
  965.                 var logins =
  966.                     this.findLogins({}, formOrigin, actionOrigin, null);
  967.  
  968.                 this.log("form[" + i + "]: got " + logins.length + " logins.");
  969.  
  970.                 previousActionOrigin = actionOrigin;
  971.             } else {
  972.                 this.log("form[" + i + "]: using logins from last form.");
  973.             }
  974.  
  975.  
  976.             // Nothing to do if we have no matching logins available.
  977.             if (logins.length == 0)
  978.                 continue;
  979.  
  980.  
  981.             // Attach autocomplete stuff to the username field, if we have
  982.             // one. This is normally used to select from multiple accounts,
  983.             // but even with one account we should refill if the user edits.
  984.             // XXX should be able to pass in |logins| to init attachment
  985.             if (usernameField)
  986.                 this._attachToInput(usernameField);
  987.  
  988.             if (autofillForm) {
  989.  
  990.                 // If username was specified in the form, only fill in the
  991.                 // password if we find a matching login.
  992.                 if (usernameField && usernameField.value) {
  993.                     var username = usernameField.value;
  994.  
  995.                     var foundLogin;
  996.                     var found = logins.some(function(l) {
  997.                                                 foundLogin = l;
  998.                                                 return (l.username == username);
  999.                                             });
  1000.                     if (found)
  1001.                         passwordField.value = foundLogin.password;
  1002.  
  1003.                 } else if (logins.length == 1) {
  1004.                     if (usernameField)
  1005.                         usernameField.value = logins[0].username;
  1006.                     passwordField.value = logins[0].password;
  1007.                 }
  1008.             }
  1009.         } // foreach form
  1010.     },
  1011.  
  1012.  
  1013.     /*
  1014.      * _attachToInput
  1015.      *
  1016.      * Hooks up autocomplete support to a username field, to allow
  1017.      * a user editing the field to select an existing login and have
  1018.      * the password field filled in.
  1019.      */
  1020.     _attachToInput : function (element) {
  1021.         this.log("attaching autocomplete stuff");
  1022.         element.addEventListener("blur",
  1023.                                 this._domEventListener, false);
  1024.         element.addEventListener("DOMAutoComplete",
  1025.                                 this._domEventListener, false);
  1026.         this._formFillService.markAsLoginManagerField(element);
  1027.     },
  1028.  
  1029.  
  1030.     /*
  1031.      * _fillPassword
  1032.      *
  1033.      * The user has autocompleted a username field, so fill in the password.
  1034.      */
  1035.     _fillPassword : function (usernameField) {
  1036.         this.log("fillPassword autocomplete username: " + usernameField.value);
  1037.  
  1038.         var form = usernameField.form;
  1039.         var doc = form.ownerDocument;
  1040.  
  1041.         var hostname = this._getPasswordOrigin(doc.documentURI);
  1042.         var formSubmitURL = this._getActionOrigin(form)
  1043.  
  1044.         // Find the password field. We should always have at least one,
  1045.         // or else something has gone rather wrong.
  1046.         var pwFields = this._getPasswordFields(form, false);
  1047.         if (!pwFields) {
  1048.             const err = "No password field for autocomplete password fill.";
  1049.  
  1050.             // We want to know about this even if debugging is disabled.
  1051.             if (!this._debug)
  1052.                 dump(err);
  1053.             else
  1054.                 this.log(err);
  1055.  
  1056.             return;
  1057.         }
  1058.  
  1059.         // XXX: we could do better on forms with 2 or 3 password fields.
  1060.         var passwordField = pwFields[pwFields.length - 1].element;
  1061.  
  1062.         // XXX this would really be cleaner if we could get at the
  1063.         // AutoCompleteResult, which has the actual nsILoginInfo for the
  1064.         // username selected.
  1065.  
  1066.         // Temporary LoginInfo with the info we know.
  1067.         var currentLogin = new this._nsLoginInfo();
  1068.         currentLogin.init(hostname, formSubmitURL, null,
  1069.                           usernameField.value, null,
  1070.                           usernameField.name, passwordField.name);
  1071.  
  1072.         // Look for a existing login and use its password.
  1073.         var match = null;
  1074.         var logins = this.findLogins({}, hostname, formSubmitURL, null);
  1075.  
  1076.         if (!logins.some(function(l) {
  1077.                                 match = l;
  1078.                                 return currentLogin.equalsIgnorePassword(l);
  1079.                         }))
  1080.         {
  1081.             this.log("Can't find a login for this autocomplete result.");
  1082.             return;
  1083.         }
  1084.  
  1085.         this.log("Found a matching login, filling in password.");
  1086.         passwordField.value = match.password;
  1087.     },
  1088.  
  1089.  
  1090.  
  1091.  
  1092.  
  1093.  
  1094.     /* ---------- User Prompts ---------- */
  1095.  
  1096.  
  1097.  
  1098.  
  1099.     /*
  1100.      * _promptToSaveLogin
  1101.      *
  1102.      * Called when we detect a new login in a form submission,
  1103.      * asks the user what to do.
  1104.      *
  1105.      * Return values:
  1106.      *   0 - Save the login
  1107.      *   1 - Ignore the login this time
  1108.      *   2 - Never save logins for this site
  1109.      */
  1110.     _promptToSaveLogin : function (aWindow) {
  1111.         const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
  1112.             (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
  1113.             (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
  1114.             (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
  1115.  
  1116.         var brandShortName =
  1117.                 this._brandBundle.GetStringFromName("brandShortName");
  1118.  
  1119.         var dialogText         = this._getLocalizedString(
  1120.                                         "savePasswordText", [brandShortName]);
  1121.         var dialogTitle        = this._getLocalizedString(
  1122.                                         "savePasswordTitle");
  1123.         var neverButtonText    = this._getLocalizedString(
  1124.                                         "neverForSiteButtonText");
  1125.         var rememberButtonText = this._getLocalizedString(
  1126.                                         "rememberButtonText");
  1127.         var notNowButtonText   = this._getLocalizedString(
  1128.                                         "notNowButtonText");
  1129.  
  1130.         this.log("Prompting user to save/ignore login");
  1131.         var result = this._promptService.confirmEx(aWindow,
  1132.                                             dialogTitle, dialogText,
  1133.                                             buttonFlags, rememberButtonText,
  1134.                                             notNowButtonText, neverButtonText,
  1135.                                             null, {});
  1136.         return result;
  1137.     },
  1138.  
  1139.  
  1140.     /*
  1141.      * _promptToChangePassword
  1142.      *
  1143.      * Called when we think we detect a password change for an existing
  1144.      * login, when the form being submitted contains multiple password
  1145.      * fields.
  1146.      *
  1147.      * Return values:
  1148.      *   true  - Update the stored password
  1149.      *   false - Do not update the stored password
  1150.      */
  1151.     _promptToChangePassword : function (aWindow, username) {
  1152.         const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
  1153.  
  1154.         var dialogText  = this._getLocalizedString(
  1155.                                     "passwordChangeText", [username]);
  1156.         var dialogTitle = this._getLocalizedString(
  1157.                                     "passwordChangeTitle");
  1158.  
  1159.         // returns 0 for yes, 1 for no.
  1160.         var result = this._promptService.confirmEx(aWindow,
  1161.                                 dialogTitle, dialogText, buttonFlags,
  1162.                                 null, null, null,
  1163.                                 null, {});
  1164.         return !result;
  1165.     },
  1166.  
  1167.  
  1168.     /*
  1169.      * _promptToChangePasswordWithUsernames
  1170.      *
  1171.      * Called when we detect a password change in a form submission, but we
  1172.      * don't know which existing login (username) it's for. Asks the user
  1173.      * to select a username and confirm the password change.
  1174.      *
  1175.      * Returns multiple paramaters:
  1176.      * [0] - User's respone to the dialog
  1177.      *   true  = Update the stored password
  1178.      *   false = Do not update the stored password
  1179.      * [1] - The username selected
  1180.      *   (null if [0] is false)
  1181.      *  
  1182.      */
  1183.     _promptToChangePasswordWithUsernames : function (aWindow, usernames) {
  1184.         const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
  1185.  
  1186.         var dialogText  = this._getLocalizedString("userSelectText");
  1187.         var dialogTitle = this._getLocalizedString("passwordChangeTitle");
  1188.         var selectedUser = null, selectedIndex = { value: null };
  1189.  
  1190.         // If user selects ok, outparam.value is set to the index
  1191.         // of the selected username.
  1192.         var ok = this._promptService.select(aWindow,
  1193.                                 dialogTitle, dialogText,
  1194.                                 usernames.length, usernames,
  1195.                                 selectedIndex);
  1196.         if (ok)
  1197.             selectedUser = usernames[selectedIndex.value];
  1198.  
  1199.         return [ok, selectedUser];
  1200.     },
  1201.  
  1202.  
  1203.     /*
  1204.      * _getLocalisedString
  1205.      *
  1206.      * Can be called as:
  1207.      *   _getLocalisedString("key1");
  1208.      *   _getLocalizedString("key2", ["arg1"]);
  1209.      *   _getLocalizedString("key3", ["arg1", "arg2"]);
  1210.      *   (etc)
  1211.      *
  1212.      * Returns the localized string for the specified key,
  1213.      * formatted if required.
  1214.      *
  1215.      */ 
  1216.     _getLocalizedString : function (key, formatArgs) {
  1217.         if (formatArgs)
  1218.             return this._strBundle.formatStringFromName(
  1219.                                         key, formatArgs, formatArgs.length);
  1220.         else
  1221.             return this._strBundle.GetStringFromName(key);
  1222.     }
  1223.  
  1224. }; // end of LoginManager implementation
  1225.  
  1226.  
  1227.  
  1228.  
  1229. // nsIAutoCompleteResult implementation
  1230. function UserAutoCompleteResult (aSearchString, matchingLogins) {
  1231.     function loginSort(a,b) {
  1232.         var userA = a.username.toLowerCase();
  1233.         var userB = b.username.toLowerCase();
  1234.  
  1235.         if (a < b)
  1236.             return -1;
  1237.  
  1238.         if (b > a)
  1239.             return  1;
  1240.  
  1241.         return 0;
  1242.     };
  1243.  
  1244.     this.searchString = aSearchString;
  1245.     this.logins = matchingLogins.sort(loginSort);
  1246.     this.matchCount = matchingLogins.length;
  1247.  
  1248.     if (this.matchCount > 0) {
  1249.         this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
  1250.         this.defaultIndex = 0;
  1251.     }
  1252. }
  1253.  
  1254. UserAutoCompleteResult.prototype = {
  1255.     QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
  1256.                                             Ci.nsISupportsWeakReference]),
  1257.  
  1258.     // private
  1259.     logins : null,
  1260.  
  1261.     // Interfaces from idl...
  1262.     searchString : null,
  1263.     searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
  1264.     defaultIndex : -1,
  1265.     errorDescription : "",
  1266.     matchCount : 0,
  1267.  
  1268.     getValueAt : function (index) {
  1269.         if (index < 0 || index >= this.logins.length)
  1270.             throw "Index out of range.";
  1271.  
  1272.         return this.logins[index].username;
  1273.     },
  1274.  
  1275.     getCommentAt : function (index) {
  1276.         return "";
  1277.     },
  1278.  
  1279.     getStyleAt : function (index) {
  1280.         return "";
  1281.     },
  1282.  
  1283.     getImageAt : function (index) {
  1284.         return "";
  1285.     },
  1286.  
  1287.     removeValueAt : function (index, removeFromDB) {
  1288.         if (index < 0 || index >= this.logins.length)
  1289.             throw "Index out of range.";
  1290.  
  1291.         var removedLogin = this.logins.splice(index, 1);
  1292.         this.matchCount--;
  1293.         if (this.defaultIndex > this.logins.length)
  1294.             this.defaultIndex--;
  1295.  
  1296.         if (removeFromDB) {
  1297.             var pwmgr = Cc["@mozilla.org/login-manager;1"]
  1298.                             .getService(Ci.nsILoginManager);
  1299.             pwmgr.removeLogin(removedLogin);
  1300.         }
  1301.     },
  1302. };
  1303.  
  1304. var component = [LoginManager];
  1305. function NSGetModule (compMgr, fileSpec) {
  1306.     return XPCOMUtils.generateModule(component);
  1307. }
  1308.